package orchestration.android;

import java.util.ArrayList;
import java.util.List;

import orchestration.android.execution.commands.AndroidOrchestrationCommand;
import orchestration.android.execution.commands.CommandFactory;
import orchestration.android.execution.commands.ManageActivityResultCommand;
import orchestration.android.execution.commands.ExecTaskCommand;
import orchestration.android.execution.exceptions.TaskExecutionException;
import orchestration.android.execution.tasks.AndroidTask;
import orchestration.android.execution.tasks.AndroidTaskFactory;
import orchestration.android.execution.tasks.EndProcessTask;
import orchestration.android.execution.tasks.StartProcessTask;
import orchestration.android.parcelable.ParcelableAndroidTaskDTO;
import orchestration.android.parcelable.ParcelableProperty;
import orchestration.businessprocess.execution.Property;
import orchestration.businessprocess.model.BPArtifact;
import orchestration.persistence.OrchestratorAuthority.AndroidProcessExecution;
import orchestration.persistence.exceptions.MalformedAnnotationException;
import orchestration.persistence.sqlite.OrchestratorDatabaseHelper;
import android.app.Activity;
import android.app.Service;
import android.content.ComponentName;
import android.content.Intent;
import android.content.ServiceConnection;
import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
import android.os.IBinder;
import android.os.RemoteException;
import android.util.Log;
import android.widget.Toast;

/**
 * An activity that, given an execution uri (passed as the data field of the intent)
 * runs the corresponding execution. To do this, it dialogues with an IBusinessProcess
 * service, registering/releasing the execution, resumeing it, or retrieving/saving
 * properties.
 * An ICallback callback is used to be notified when a task has to be executed.
 * As some methods will be called asynchronously (and we don't know calls order),
 * pattern Command id used to isolate the code that executes an operation from the
 * code that requests its execution.
 * A locking key is used to be sure that, once somebody registers to run an execution,
 * only that component is allowed to change the execution state. (used to avoid
 * inconsistency due to recursion over the same execution).
 * @author  <a href="mailto:car.ferrarini@gmail.com">Carlo Ferrarini</a>
 * 
 * -input: all the {@link BPArtifact} that where drawn as output of the StartEvent
 * (will be ignored if the execution is not new)
 * -output: all the {@link BPArtifact} that where drawn as input of the EndEvent
 * (will be ignored if the execution will not reach its end)
 */
public class RunExecutionActivity extends Activity {
   
	private static final String TAG="RunExecutionActivity";
	
	private static final String EXECUTION_URI="execution_uri";
	private static final String BPI_KEY="bpi_key";
	private static final String COMMAND_VALUE = "on_service_connected_command";
	
	/**
	 * @uml.property  name="mService"
	 * @uml.associationEnd  
	 */
	private IBusinessProcess mService;

	//state of this Activity
	/**
	 * @uml.property  name="currentCommand"
	 * @uml.associationEnd  
	 */
	private AndroidOrchestrationCommand currentCommand;
	/**
	 * @uml.property  name="lockingKey"
	 */
	private String lockingKey;
	/**
	 * @uml.property  name="mUri"
	 */
	private Uri mUri;
	/**
	 * @uml.property  name="currentTask"
	 * @uml.associationEnd  
	 */
	private AndroidTask currentTask;//<-- no need to save this, you can retrieve infos to create it from the service
	
//  <---------------------- ACTIVITY STATE AND LIFECYCLE MANAGEMENT METHODS ------------------------> 
	/**
	 * initialize the Activity state, eventually reading it from a previous savedInstanceState
	 * if the execution's status is "ended" the Activity is finished
	 */
    @Override
    public void onCreate(Bundle savedInstanceState) {
    	super.onCreate(savedInstanceState);
    	mUri= getIntent().getData();
    	int commandId=0;
    	if(savedInstanceState!=null){
    		this.mUri=Uri.parse(savedInstanceState.getString(EXECUTION_URI));
    		this.lockingKey=savedInstanceState.getString(BPI_KEY);
			commandId=savedInstanceState.getInt(COMMAND_VALUE);
    	}
    	if(commandId==0)
    		commandId= CommandFactory.REPEAT_LAST;//default command
    	if(currentCommand==null)
    		currentCommand=CommandFactory.createCommand(commandId);
    	//retrieve the process execution
    	Cursor c= managedQuery(mUri, OrchestratorDatabaseHelper.EXECUTION_FIELDS, null, null, null);
    	c.moveToFirst();
    	if(c.isAfterLast()){
    		handleException(new NullPointerException("Process execution not found"));
    		return;
    	}
    	//check the status
    	String status = c.getString(c.getColumnIndex(AndroidProcessExecution.EXECUTION_STATUS));
    	if(status.equals("ended")){
			handleException(new NullPointerException("The execution ended"));
    	}
    }
   /**
    * save the state of the activity
    */
	@Override
    protected void onSaveInstanceState(Bundle outState) {
    	super.onSaveInstanceState(outState);
    	outState.putString(EXECUTION_URI, mUri.toString());
    	outState.putString(BPI_KEY, lockingKey);
    	if(currentCommand!=null)
    		outState.putInt(COMMAND_VALUE, currentCommand.getId());
    }
    /**
     * eventually unbind from the service, so that it could be destroyed in case of low memory
     */
    @Override
    protected void onPause() {
    	super.onPause();
    	if(mService!=null){
    		mService=null;
    		unbindService(conn);
    	}
    }
   /**
    * bind to the service
    */
    @Override
    protected void onResume() {
    	super.onResume();
    	bindToBusinessProcessService();
    }
    /**
     * when ready, handle the result received from the "child" activity
     */
    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
    	currentCommand= new ManageActivityResultCommand(resultCode,data);
    }
    @Override
    public void onBackPressed() {
    	releaseExecution();
    	super.onBackPressed();
    }

    /**
     * binds to BusinessProcess  service if mService is null
     * (can happen if the service was never created, or if it was destroyed by Android due to low memory)
     */
	private void bindToBusinessProcessService() {
		if(mService==null){
	    	Intent serviceIntent= new Intent(getIntent().getAction(),getIntent().getData());
	    	bindService(serviceIntent, conn,Service.BIND_AUTO_CREATE);
    	}
	}
//  <-------------- TASK EXECUTON SUPPORT: PROPERTY MANAGEMENT METHODS ------------------>
	/**
	 * retrieve a Property (calling IBusinessProcess service)
	 * @param propName the name of the property
	 */
	public Property getProperty(String propName) {
		try {
			if(propName!=null)
				return mService.getProperty(mUri,propName);
		} catch (RemoteException e) {
			handleException(e);
		}
		return null;
	}
	/**
	 * saves a Property (calling IBusinessProcess service)
	 * @param property
	 */
	public void saveProperty(Property property) {
		try {
			mService.saveProperty(mUri,lockingKey,(ParcelableProperty)property);
		} catch (RemoteException e) {
			handleException(e);
		}
	}
	/**
	 * retrieve a List of Properties given a List of names (calling IBusinessProcess service)
	 * @param propNames the names of the Properties to retrieve
	 * @return the requested Properties
	 */
	public List<Property> getProperties(
			List<String> propNames) {
		List<Property> result= new ArrayList<Property>();
		for(String i : propNames)
			result.add(getProperty(i));
		return result;
	}

	/**
	 * Saves a list of {@link Property} (calling {@link IBusinessProcess} service)
	 * @param properties the properties to save
	 */
	public void saveProperties(List<Property> properties) {
		for(Property p : properties)
			saveProperty(p);
	}
	
//   <------------------ EXECUTION MANAGEMENT METHODS -------------------->	
	 /**
	 * setter for currentCommand instance variable
	 * @param currentCommand
	 * @uml.property  name="currentCommand"
	 */
    public void setCurrentCommand(AndroidOrchestrationCommand command) {
		this.currentCommand = command;
	}
    /**
     * executes the currentCommand
     */
	public void executeNextCommand() {
		currentCommand.execute(this);
		currentCommand=null;
	}
	
	/**
	 * retrieves the currentTask, eventually re-creating it (calling {@link IBusinessProcess} service)
	 * @return the CurrentTask
	 * @uml.property  name="currentTask"
	 */
	public AndroidTask getCurrentTask() {
		if(currentTask==null)
			try {
				currentTask=AndroidTaskFactory.createAndroidTask(mService.getLastNodeInfo(mUri, lockingKey));
			} catch (RemoteException e) {
				handleException(e);
				return null;
			} catch (MalformedAnnotationException e) {
				handleException(e);
				return null;
			}
		return currentTask;
	}
	/**
	 * setter for currentTask
	 * @param task
	 * @uml.property  name="currentTask"
	 */
	public void setCurrentTask(AndroidTask task) {
		this.currentTask=task;
	}
	/**
	 * unlicks the execution (calls {@link IBusinessProcess} service)
	 */
	public void releaseExecution() {
		try {
    		if(mService!=null)
    			mService.releaseExecution(mUri, lockingKey);
		} catch (RemoteException e) {
			handleException(e);
		}
	}
	/**
	 * ask to {@link IBusinessProcess} service to resume the execution.
	 * @param repeatLast
	 */
	public void resumeExecution(boolean repeatLast) {
		try {
			mService.execute(mUri, lockingKey,repeatLast);
		} catch (RemoteException e) {
			handleException(e);
		}
	}
	/**
	 * a method to handle the exceptions thrown during execution. In this case just show a popup and call {@link Exception}.printStackTrace
	 * @param exception
	 */
	public void handleException(Exception exception) {
		exception.printStackTrace();
		Toast.makeText(this, exception.getMessage(), Toast.LENGTH_SHORT).show();
		setResult(RESULT_CANCELED);
		finish();
	}
	
//   <---------------------- CALLBACK IMPLEMENTATION METHODS ------------------------> 	
	/**
	 * notification received: the execution reached its end.
	 * creates an {@link EndProcessTask} and calls its "onExecute" method
	 * (no need to call {@link IBusinessProcess} service)
	 * @param dto data needed to create the task
	 * (contains the properties to insert as extras in the result intent)
	 */
	void onEndImpl(ParcelableAndroidTaskDTO dto) {
		Toast.makeText(RunExecutionActivity.this, "exec ended", Toast.LENGTH_SHORT).show();
		currentTask=AndroidTaskFactory.createEndProcessTask(dto);
		try {
			currentTask.onExecute(this);
		} catch (TaskExecutionException e) {
			handleException(e);
		}
		releaseExecution();
		finish();
	}
    /**
	 * notification received: the execution is new and its input must be initialized
	 * creates a {@link StartProcessTask} and calls its "onExecute" method
	 * (no need to call {@link IBusinessProcess} service)
     * @param dto data needed to create the task
     * (contains the properties to read from the input intent)
	 */
    void onStartProcessingImpl(ParcelableAndroidTaskDTO dto) {
		Toast.makeText(RunExecutionActivity.this, "Start!!!",Toast.LENGTH_SHORT).show();
		currentTask=AndroidTaskFactory.createStartProcessTask(dto);
		try {
			currentTask.onExecute(this);
		} catch (TaskExecutionException e) {
			handleException(e);
		}
		setCurrentCommand(CommandFactory.createCommand(CommandFactory.RESUME_EXECUTION));
		executeNextCommand();
	}
    /**
     * notification received: need to execute an {@link AndroidTask}
     * creates a new {@link ExecTaskCommand} (given dto data), checks if the Activity is
     * currently bound to {@link IBusinessProcess} service and eventually executes the command.
     * @param dto data needed to create the task
     */
	void onTaskImpl(ParcelableAndroidTaskDTO dto) {
		currentCommand= new ExecTaskCommand(dto);
		if(mService==null)
			bindToBusinessProcessService();
		else
			executeNextCommand();
	}
	/**
	 * notification received: the execution was locked succesfully
	 * @param key the lockingKey used to lock the execution
	 */
	void onRegistrationSuccededImpl(String key) {
		lockingKey=key;
		executeNextCommand();
	}
	/**
	 * notification received: need to manage an event, sending or receiving a broadcast
	 * @param dto data needed to create the task
	 */
	void onIntEventImpl(ParcelableAndroidTaskDTO dto) {
		AndroidTask task=null;
		try {
			task = AndroidTaskFactory.createBroadcastTask(dto);
		} catch (MalformedAnnotationException e) {
			handleException(e);
			return;
		}
		if(task!=null){
			currentTask=task;
			try {
				task.onExecute(RunExecutionActivity.this);
			} catch (TaskExecutionException e) {
				handleException(e);
			}
		}
	}

	/**
	 * The callback that is sent when registering for an execution. Using a callback instead
	 * of receiving a returnValue directly will avoid blocking calls 
	 * @uml.property  name="callback"
	 * @uml.associationEnd  
	 */
	private ICallback callback= new ICallback.Stub(){

		@Override
		public void onEndProcessing(ParcelableAndroidTaskDTO dto) throws RemoteException {
			onEndImpl(dto);
			
		}
		
		@Override
		public void onIntEvent(ParcelableAndroidTaskDTO dto) {
			onIntEventImpl(dto);
		}
		
		@Override
		public void onStartProcessing(ParcelableAndroidTaskDTO dto) throws RemoteException {
			onStartProcessingImpl(dto);
		}
		
		@Override
		public void onTask(ParcelableAndroidTaskDTO dto) throws RemoteException {
			onTaskImpl(dto);
		}
		@Override
		public void onRegistrationSucceded(String key) throws RemoteException {
			onRegistrationSuccededImpl(key);
		}
		@Override
		public IBinder asBinder() {
			return this;
		}
    };
    /**
     * connection used to bind to a {@link IBusinessProcess} service.
     * 
     */
	private ServiceConnection conn=new ServiceConnection() {
		@Override
		public void onServiceDisconnected(ComponentName name) {
			Log.w(TAG, "on service disconnected");
			mService=null;
		}
		/**
		 * if it is the first time that we bind, register for execution,
		 * else execute the pending command.
		 */
		@Override
		public void onServiceConnected(ComponentName name, IBinder ibinder) {
	    	Log.w(TAG, "onServiceConnected() ");
			if(ibinder==null)
				handleException(new NullPointerException("Unable to bind to the service"));
			else{
				mService= IBusinessProcess.Stub.asInterface(ibinder);
				try {
					if(lockingKey==null){
						mService.registerForExecution(mUri, callback);
					}else
						executeNextCommand();
				} catch (RemoteException e) {
					handleException(e);
				}
			}
		}
	};
}